// nth-graph-view jquery plugin
// author: Nate Morse copyright 2009
// BSD license
// version: 0.02
// project site: http://code.google.com/p/nth-graph/
// source repository:
// http://nth-graph.googlecode.com/svn/trunk/nth-graph/ide/svg_based
// usage:
//   $("#svgElement").nthGraphView({options});
// options:
//   g:object nth-graph in JSON or JSOL
//   gv:object nth-graph-view in JSON or JSOL
//   warn:function calls a function when a warning is encoutered default:console
//   debug:function called for debuging

(function($) {

    $.fn.nthGraphView = function(settings) {
        var config = {
            "g":{},
            "gv":{"graph":{"name":"1"},
                "defaultNode":{"shape":"circle", "r":25, "padding":0},
                "defaultEdge":{"magnitude":30, "arrow":true, "tail":{"angle":0}, "head":{"angle":180} }
            },
            "gvEvents":{
                "allEdges":{
                    "mouseup":function(click_ev) {
                        if (!click_ev) {var click_ev = window.event;}
                        var i = nth.graphView.getIndexFromId(click_ev.target.id, "edge");
                        var edge = nth.graph.edges[i];
                        var edge_view = $.extend(true, {}, config.gv.defaultEdge, nth.view.edges[i]);
                        var node_view1 = $.extend(true, {}, config.gv.defaultNode, nth.view.nodes[edge.from]);
                        var node_view2 = $.extend(true, {}, config.gv.defaultNode, nth.view.nodes[edge.to]);
                        var cp1 = nth.getConnectionPoint('tail', node_view1, edge_view);
                        var cp2 = nth.getConnectionPoint('head', node_view2, edge_view);

                        // end any previous edge edit session.
                        nth.graphView.selected_cp = false;
                        $('#nth-'+config.gv.graph.name+'-cp-0, #nth-'+config.gv.graph.name+'-cp-1').remove();
                        $('#'+nth.graphEdit.deleteIconId()).remove();
                        nth.graphView.selected_edge = {'index':i};
                        nth.graphEdit.showDeleteEdgeIcon(cp1, cp2);
                        nth.graphView.showCP(cp1[1].x, cp1[1].y, 0);
                        nth.graphView.showCP(cp2[1].x, cp2[1].y, 1);
                        nth.graphView.exportEdgeProperties(i);
                    }
                },
                "allNodes":{
                    "mouseover":function(roll_ev) {
                        if (nth.graphView.selected_node){return;}
                        if (!roll_ev) {var roll_ev = window.event;}
                        var i = nth.graphView.getIndexFromId(roll_ev.target.id, "node");
                        if (nth.graphView.new_edge_mode){nth.graphView.new_edge_mode.to = i; return;}
                        nth.graphView.showNodeContextMenu(i, roll_ev.target.id);

                    },
                    "mousedown":function(click_ev) {
                        if (!click_ev) {var click_ev = window.event;}
                        var i = nth.graphView.getIndexFromId(click_ev.target.id, "node");
                        var x = nth.view.nodes[i].cx - click_ev.pageX;
                        var y = nth.view.nodes[i].cy - click_ev.pageY;
                        nth.graphView.selected_node = {'index':i, 'offset':[x,y]};

                        // end an edge edit session.
                        nth.graphView.selected_cp = false;
                        $('#nth-'+config.gv.graph.name+'-cp-0, #nth-'+config.gv.graph.name+'-cp-1').remove();
                        $('#'+nth.graphEdit.deleteIconId()).remove();
                    },
                    "mousemove":function(click_ev) {
                        if (!click_ev) {var click_ev = window.event;}
                        var i;
                        var sel = nth.graphView.selected_node;
                        if (sel) {
                            i = sel.index;
                            nth.view.nodes[i].cx = click_ev.pageX+sel.offset[0];
                            nth.view.nodes[i].cy = click_ev.pageY+sel.offset[1];
                            nth.graphView.show(false);
                            //previously this did the job nth.graphView.show(false);
                            //nth.graph.nodes[i].view.cx = click_ev.pageX+sel.offset[0];
                            //nth.graph.nodes[i].view.cy = click_ev.pageY+sel.offset[1];
                            //click_ev.target.setAttribute('cx', click_ev.pageX+sel.offset[0]);
                            //click_ev.target.setAttribute('cy', click_ev.pageY+sel.offset[1]);
                        }
                    },
                    "mouseup":function(click_ev) {
                       nth.graphView.selected_node = false;
                       nth.graphView.selected_edge = false;
                       nth.graphView.selected_cp = false;
                       if (nth.graphView.new_edge_mode){
                            // finalize the new edge
                            nth.graph.edges.push(nth.graphView.new_edge_mode);
                            nth.view.edges.push({"tail":{}, "head":{}});
                            nth.graphView.new_edge_mode = false;
                            $(nth.graphView.omee).empty();
                            nth.graphView.show(false);
                       }
                    }
                },
                "omee":{
                    "mousedown":function(click_ev) {
                        if (nth.graphView.selected_edge) {
                            nth.graphView.selected_cp = false;
                            $('#nth-'+config.gv.graph.name+'-cp-0, #nth-'+config.gv.graph.name+'-cp-1').remove();
                            $('#'+nth.graphEdit.deleteIconId()).remove();
                            return;
                        }
                        if (!click_ev) {var click_ev = window.event;}
                        var i = nth.graph.nodes.length;
                        var x =  30 - click_ev.pageX;
                        var y =  30 - click_ev.pageY;

                        // end any previous edge edit session.
                        nth.graphView.selected_cp = false;
                        $('#nth-'+config.gv.graph.name+'-cp-0, #nth-'+config.gv.graph.name+'-cp-1').remove();
                        $('#'+nth.graphEdit.deleteIconId()).remove();

                        if (nth.graphView.new_node_mode) {
                            nth.graph.nodes[i] = {"name":"new"};
                            nth.view.nodes[i] = {"cx":30, "cy":30};
                            nth.graphView.selected_node = {'index':i, 'offset':[x,y]};
                        }
                        $(nth.graphView.omee).empty();
                        nth.graphView.show(false);
                    },
                    "mousemove":function(click_ev) {
                        if (!click_ev) {var click_ev = window.event;}
                        var i;
                        var sel_node = nth.graphView.selected_node;
                        if (sel_node) {
                            i = sel_node.index;
                            nth.view.nodes[i].cx = click_ev.pageX + sel_node.offset[0];
                            nth.view.nodes[i].cy = click_ev.pageY + sel_node.offset[1];
                            nth.graphView.show(false);
                        }
                        if (nth.graphView.selected_cp) {
                            nth.cpAdjustEdge(click_ev);
                        }
                    },
                    "mouseup":function(click_ev) {
                       nth.graphView.selected_node = false;
                       nth.graphView.selected_edge = false;
                       nth.graphView.selected_cp = false;
                    }
                },
                "nodeLabel":{
                    "mousemove":function(click_ev) {
                        if (!click_ev) {var click_ev = window.event;}
                        click_ev.cancelBubble = true;
                        if (click_ev.stopPropagation) {click_ev.stopPropagation();}
                    },
                     "mouseup":function(click_ev) {
                        if (!click_ev) {var click_ev = window.event;}
                           //nth.graphView.selected_node = false;
                        nth.graphView.selected_edge = false;
                        var i = nth.graphView.getIndexFromId(click_ev.target.id, "node");
                        if (nth.graphView.new_edge_mode) {
                            nth.graphView.new_edge_mode.to = i;
                            // finalize the new edge
                            nth.graph.edges.push(nth.graphView.new_edge_mode);
                            nth.view.edges.push({"tail":{}, "head":{}});
                            nth.graphView.new_edge_mode = false;
                            nth.graphView.exportEdgeProperties(nth.view.edges.length - 1);
                            $(nth.graphView.omee).empty();
                            nth.graphView.show(false);
                        }
                        else {
                            nth.graphView.exportNodeProperties(i);
                        }
                    }
                }
            },
            "warn": function(msg){alert(msg);},
            "debug": function(msg){}
        };

        var nth = {
            graph:{},
            view:{},
            graphEdit: {
                deleteIconId: function () {
                    return 'nth-'+config.gv.graph.name+'-edge-delete';
                },
                showDeleteEdgeIcon: function(cp1, cp2) {
                    var cp = {x:0, y:0};
                    var mp = [];
                    var el = document.createElementNS(svgns, 'circle');
                    var r = 6;
                    var fill = '#FF4444';
                    var stroke = 'black';

                    mp.push(nth.graphEdit.midPoint(cp1[1], cp1[0]));
                    //mp.push(nth.graphEdit.midPoint(cp1[0], cp2[0]));
                    mp.push(nth.graphEdit.midPoint(cp2[0], cp2[1]));
                    //mp.push(nth.graphEdit.midPoint(cp2[1], cp1[1]));
                    cp = nth.graphEdit.midPoint(mp[0], mp[1]);
                    
                    el.setAttribute('class', 'nth-cp');
                    el.setAttribute('id', nth.graphEdit.deleteIconId());
                    el.setAttribute('cx', cp.x);
                    el.setAttribute('cy', cp.y);
                    el.setAttribute('r', r);
                    el.setAttribute('fill', fill);
                    el.setAttribute('stroke-width', '1px');
                    el.setAttribute('stroke', stroke);
                    nth.graphView.omee.appendChild(el);
                    $(el).bind("mouseup", function(click_ev) {
                        if (nth.graphView.selected_cp) {return;}
                        var sel = nth.graphView.selected_edge;
                        nth.graphView.selected_cp = false;
                        nth.removeEdge(sel.index);
                        $(nth.graphView.omee).empty();
                        nth.graphView.show(false);
                    });

                },
                midPoint: function(a, b) {
                    return {x:(b.x - a.x)/2 + a.x, y:(b.y - a.y)/2 + a.y};
                }
            },
            graphView: {
                new_node_mode: true,
                selected_node: false, // for node move, not for node property selection... Todo copy or rename
                selected_edge: false,
                new_edge_mode: false,
                selected_cp: false,
                omee:null,
                exportNodeProperties: function(i){
                    $("body").trigger("export-node-properties", [i, nth.graph.nodes[i]]);
                },
                exportEdgeProperties: function(i){
                    $("body").trigger("export-edge-properties", [i, nth.graph.edges[i]]);
                },
                show: function(omee) {
                    //if (nth.key_exists("allNodes", config.gvEvents)) {
                        //$.each(config.gvEvents.allNodes, function(ev_index, ev) {
                            //nth.graphView.gvEvent.allNodes.append;
                        //});
                    //}
                    nth.debug("start show...");
                    if (omee) {
                        nth.attachEnv(omee);
                    }
                    nth.graphView.traverse();
                    // bind events for
                    // all Edges
                    if (nth.key_exists("allEdges", config.gvEvents)) {
                        $.each(nth.graph.edges, function(edge_index, edge) {
                           $.each(config.gvEvents["allEdges"], function(gev_index, gev) {
                                var dom_id = "nth-"+config.gv.graph.name+"-edge-"+edge_index;
                                var ele = document.getElementById(dom_id);
                                if (ele) {
                                    ele.addEventListener(gev_index, gev, false);
                                }
                                //else {
                                //    nth.warn("no element id = "+dom_id)
                                //}
                                dom_id = "nth-"+config.gv.graph.name+"-label-edge-"+edge_index;
                                ele = document.getElementById(dom_id);
                                if (ele) {
                                    ele.addEventListener(gev_index, gev, false);
                                }
                                //else {
                                //    nth.warn("no element id = "+dom_id)
                                //}
                            });
                          });
                    }
                    // all Nodes
                    if (nth.key_exists("allNodes", config.gvEvents)) {
                        $.each(nth.graph.nodes, function(node_index, node) {
                            $.each(config.gvEvents["allNodes"], function(gev_index, gev) {
                                var dom_id = "nth-"+config.gv.graph.name+"-node-"+node_index;
                                var ele = document.getElementById(dom_id);
                                if (ele) {
                                    ele.addEventListener(gev_index, gev, false);
                                }
                                //else {
                                //    nth.warn("no element id = "+dom_id)
                                //}
                            });
                          });
                    }
                    // node Label
                    if (nth.key_exists("nodeLabel", config.gvEvents)) {
                        $.each(nth.graph.nodes, function(node_index, node) {
                           $.each(config.gvEvents["nodeLabel"], function(gev_index, gev) {
                                var dom_id = "nth-"+config.gv.graph.name+"-label-node-"+node_index;
                                var ele = document.getElementById(dom_id);
                                if (ele) {
                                    ele.addEventListener(gev_index, gev, false);
                                }
                            });
                          });
                    }
                    // outer most environment element (omee)
                    if (nth.key_exists("omee", config.gvEvents)) {
                        $.each(config.gvEvents["omee"], function(gev_index, gev) {
                            document.getElementById('nth-'+config.gv.graph.name+'-backdrop').addEventListener(gev_index, gev, false);
                            //nth.graphView.omee.addEventListener(gev_index, gev, false);
                        });
                    }

                    nth.detachEnv();
                },
                traverse: function() {
                    // collect in a fragment. (note the extra 'true' argument is for use in SVG Web)
                    var frag = document.createDocumentFragment(true);
                    // insert a backdrop to collect all mouse droppings
                    // <rect id="cedar-shavings" width="100%" height="100%"/>
                    var backdrop_id = 'nth-'+config.gv.graph.name+'-backdrop';
                    var backdrop = document.getElementById(backdrop_id);
                    var create = false;
                    if (!backdrop) {backdrop = document.createElementNS(svgns, 'rect'); create = true;}
                    backdrop.setAttribute('id', backdrop_id);
                    backdrop.setAttribute('width', '100%');
                    backdrop.setAttribute('height', '100%');
                    if (create) {frag.appendChild(backdrop);}
                    if (!nth.get_property(nth.view, "edges")) {nth.view.edges = [];}
                    $.each(nth.graph.edges, function(edge_index, edge) {
                        // write back to the view (make an edge object) to fill in a blank view
                        if (!nth.get_property(nth.view, "edges", edge_index)) {
                            nth.view.edges[edge_index] = {"tail":{}, "head":{}};
                        }
                        // draw this edge
                        var edge_view = $.extend(true, {}, config.gv.defaultEdge, nth.view.edges[edge_index]||{});
                        var nv1 = $.extend(true, {}, config.gv.defaultNode, nth.get_property(nth.view, "nodes", edge.from)||{});
                        var nv2 = $.extend(true, {}, config.gv.defaultNode, nth.get_property(nth.view, "nodes", edge.to)||{});
                        var fromCP = nth.getConnectionPoint("tail", nv1, edge_view);
                        var toCP = nth.getConnectionPoint("head", nv2, edge_view);
                        var elements = nth.renderEdge(fromCP, toCP, edge, edge_view, edge_index);
                        if (elements[0]) {
                            frag.appendChild(elements[0]);
                        }
                        if (elements[1]) {
                            frag.appendChild(elements[1]);
                        }
                        if (elements[2]) {
                            frag.appendChild(elements[2]);
                        }

                    });
                    $.each(nth.graph.nodes, function(node_index, node) {
                        // draw this node
                        var node_view = $.extend(true, {}, config.gv.defaultNode, nth.get_property(nth.view, "nodes", node_index)||{});
                        // maybe this next line does not display anything because of the wrong namespace??
                        //$(nth.graphView.omee).append('<circle id="node_'+node_index+'" cx="'+((node_index*25)+20)+'" cy="75" r="10" stroke="black" stroke-width="1" />');
                        var el_id = 'nth-'+config.gv.graph.name+'-node-'+node_index;
                        var el = document.getElementById(el_id);
                        var create = (!el);

                        if (create) {el = document.createElementNS(svgns, 'circle');}
                        var cx = node_view.cx? node_view.cx: 45*((node_index%4)+1)+30;
                        var cy = node_view.cy? node_view.cy: 30*(Math.floor(node_index/4)+1);
                        // write back to view (cx, cy) to fill in a blank view
                        if (!nth.get_property(nth.view, "nodes")) {nth.view.nodes = [];}
                        if (!nth.get_property(nth.view, "nodes", node_index)) {
                            nth.view.nodes[node_index] = {"cx":cx, "cy":cy};
                        }
                        var r = node_view.r? node_view.r: 30;
                        var fill = node_view.fill? node_view.fill: '#FBF7F1';
                        var stroke = node_view.stroke? node_view.stroke: 'black';
                        el.setAttribute('class', 'nth-node');
                        el.setAttribute('id', el_id);
                        el.setAttribute('cx', cx);
                        el.setAttribute('cy', cy);
                        el.setAttribute('r', r);
                        el.setAttribute('fill', fill);
                        el.setAttribute('stroke-width', '1px');
                        el.setAttribute('stroke', stroke);
                        if (create) {frag.appendChild(el);}

                        var label_id = 'nth-'+config.gv.graph.name+'-label-node-'+node_index;
                        var label = document.getElementById(label_id);
                        if (!label){label = document.createElementNS(svgns, 'text');}
                        label.setAttribute('class', 'nth-node nth-label');
                        label.setAttribute('id', label_id);
                        label.setAttribute('x', cx);
                        label.setAttribute('y', cy+5);
                        label.setAttribute('fill', "#222");
                        var label_text = node.ele? node.ele: "";
                        label_text = node.name? node.name: label_text;
                        label_text = label_text==""? "node: "+node_index: label_text;
                        $(label).text(label_text);
                        if (create) {frag.appendChild(label);}
                    });
                    if (create) {nth.graphView.omee.appendChild(frag);}
                    // ToDo send some messages here instead of hard coding...
                    $("#diagram-"+config.gv.graph.name+"-gv-text").text(JSON.stringify(nth.view, null, '  '));
                    $("#diagram-"+config.gv.graph.name+"-g-text").text(JSON.stringify(nth.graph, null, '  '));
                },
                getIndexFromId: function(id_string, prefix) {
                    var match_arr = id_string.match(prefix+"-([0-9]+)");
                    return match_arr[1];
                },
                getNodeConnectPoint: function(node_index) {
                    var node_view = nth.view.nodes[node_index];
                    var cpx = node_view.cx? node_view.cx: 100;
                    var cpy = node_view.cy? node_view.cy: 100;
                    return [cpx, cpy];
                },
                showCP: function(cx, cy, index) {
                    var el = document.createElementNS(svgns, 'circle');
                    var r = 6;
                    var fill = '#BBBBF1';
                    var stroke = 'black';
                    var cp_id = 'nth-'+config.gv.graph.name+'-cp-'+index;

                    el.setAttribute('class', 'nth-cp');
                    el.setAttribute('id', cp_id);
                    el.setAttribute('cx', cx);
                    el.setAttribute('cy', cy);
                    el.setAttribute('r', r);
                    el.setAttribute('fill', fill);
                    el.setAttribute('stroke-width', '1px');
                    el.setAttribute('stroke', stroke);
                    nth.graphView.omee.appendChild(el);
                    $(el).bind("mousedown", function(click_ev) {
                        if (!click_ev) {var click_ev = window.event;}
                        var i = nth.graphView.getIndexFromId(click_ev.target.id, "cp");
                        var sel = nth.graphView.selected_edge;

                        if (sel) {
                            var x = click_ev.target.cx.baseVal.value - click_ev.pageX;
                            var y = click_ev.target.cy.baseVal.value - click_ev.pageY;
                            nth.graphView.selected_cp = {'index':i, 'offset':[x,y]};
                        }
                    });
                    $(el).bind("mousemove", function(click_ev) {
                        if (!click_ev) {var click_ev = window.event;}
                        nth.cpAdjustEdge(click_ev);
                    });
                    $(el).bind("mouseup", function(click_ev) {
                       nth.graphView.selected_cp = false;
                    });

                },
                showNodeContextMenu: function (node_index, dom_id) {
                    var cx, cy, r;
                    var frag = document.createDocumentFragment(true);
                    var group = document.createElementNS(svgns, 'g');
                    var big_circle = document.createElementNS(svgns, 'circle');
                    var menu1 = document.createElementNS(svgns, 'path');
                    var menu2 = document.createElementNS(svgns, 'path');
                    var menu3 = document.createElementNS(svgns, 'path');
                    var menu4 = document.createElementNS(svgns, 'path');

                    var stroke = 'black';
                    var ncm_id = 'nth-'+config.gv.graph.name+'-ncm-'+node_index;

                    var target_node = $.extend(false, {}, config.gv.defaultNode, nth.view.nodes[node_index]);
                    cx = target_node.cx;
                    cy = target_node.cy;
                    r = target_node.r+5;
                    group.setAttribute('class', 'nth-ncm');
                    group.setAttribute('id', ncm_id);
                    group.setAttribute('transform', "translate("+cx+","+cy+")");
                    big_circle.setAttribute('cx', 0);
                    big_circle.setAttribute('cy', 0);
                    big_circle.setAttribute('r', r+8);
                    big_circle.setAttribute('opacity', '0.001');
                    big_circle.setAttribute('id', ncm_id+"-bs");


                    // "Arc to has these arguments" A rx ry x-axis-rotation large-arc-flag sweep-flag x y
                    menu1.setAttribute('d', "M0 0 L0 -"+r+" A"+r+" "+r+" 90 0 1 "+r+"  0 Z");
                    menu2.setAttribute('d', "M0 0 L"+r+"  0 A"+r+" "+r+" 90 0 1 0  "+r+" Z");
                    menu3.setAttribute('d', "M0 0 L0  "+r+" A"+r+" "+r+" 90 0 1 -"+r+" 0 Z");
                    menu4.setAttribute('d', "M0 0 L-"+r+" 0 A"+r+" "+r+" 90 0 1 0 -"+r+" Z");

                    menu1.setAttribute('fill', '#BBBBF1');
                    menu1.setAttribute('opacity', '0.4');
                    menu1.setAttribute('id', ncm_id+"-menu-1");
                    //menu1.setAttribute('stroke-width', '1px');
                    //menu1.setAttribute('stroke', stroke);
                    menu2.setAttribute('fill', '#BBF1BB');
                    menu2.setAttribute('opacity', '0.4');
                    menu2.setAttribute('id', ncm_id+"-menu-2");
                    //menu2.setAttribute('stroke-width', '1px');
                    //menu2.setAttribute('stroke', stroke);
                    menu3.setAttribute('fill', '#EEEE44');
                    menu3.setAttribute('opacity', '0.4');
                    menu3.setAttribute('id', ncm_id+"-menu-3");
                    //menu3.setAttribute('stroke-width', '1px');
                    //menu3.setAttribute('stroke', stroke);
                    menu4.setAttribute('fill', '#F1BBBB');
                    menu4.setAttribute('opacity', '0.4');
                    menu4.setAttribute('id', ncm_id+"-menu-4");
                    //menu4.setAttribute('stroke-width', '1px');
                    //menu4.setAttribute('stroke', stroke);
                    group.appendChild(big_circle);
                    group.appendChild(menu1);
                    group.appendChild(menu2);
                    group.appendChild(menu3);
                    group.appendChild(menu4);

                    frag.appendChild(group);
                    //nth.graphView.omee.appendChild(frag);
                    $("#"+dom_id).after(frag);

                    $("#"+ncm_id+"-bs").bind("mouseout", function(){$("#"+ncm_id).remove();});
                    $("#"+ncm_id+"-menu-1").bind("mousedown", function(){
                        nth.graphView.new_edge_mode = {"from":node_index, "to":-1};
                    });
                    $("#"+ncm_id+"-menu-2").bind("mousedown", function(click_ev){
                        var x = nth.view.nodes[node_index].cx - click_ev.pageX;
                        var y = nth.view.nodes[node_index].cy - click_ev.pageY;
                        nth.graphView.selected_node = {'index':node_index, 'offset':[x,y]};
                        $("#"+ncm_id).remove();
                    });
                    $("#"+ncm_id+"-menu-3").bind("click", function(){alert("Coming soon: visual properties editor, for size, shape, color and other style features.");});
                    $("#"+ncm_id+"-menu-4").bind("click", function(){
                        nth.removeNode(node_index);
                        $("#"+ncm_id).remove();
                    });
                }
            },
            cpAdjustEdge: function (click_ev) {
                var edge_i;
                var sel_edge = nth.graphView.selected_edge;
                var node_view, angle, mag, r;
                var cpx, cpy;
                if (sel_edge) {
                    edge_i = sel_edge.index;
                }
                else {return;}
                var i;
                var sel_cp = nth.graphView.selected_cp;
                var cp_id, cp;
                if (sel_cp) {
                    i = sel_cp.index;
                    cp_id = 'nth-'+config.gv.graph.name+'-cp-'+i;
                    cp = document.getElementById(cp_id);
                    cpx = click_ev.pageX+sel_cp.offset[0];
                    cpy = click_ev.pageY+sel_cp.offset[1];
                    cp.setAttribute('cx', cpx);
                    cp.setAttribute('cy', cpy);
                    if (i == 0) {
                        node_view = nth.view.nodes[nth.graph.edges[edge_i].from];
                    }
                    else {
                        node_view = nth.view.nodes[nth.graph.edges[edge_i].to];
                    }
                    angle = nth.angleFromDelta(cpx-node_view.cx, cpy-node_view.cy);
                    r = node_view.r || config.gv.defaultNode.r;
                    mag = Math.max(2, nth.magnitudeFromDelta(cpx-node_view.cx, cpy-node_view.cy)-r);

                    if (i == 0) {
                        nth.view.edges[edge_i].tail.angle = Math.floor(angle);
                        nth.view.edges[edge_i].tail.mag = Math.floor(mag);
                    }
                    else {
                        nth.view.edges[edge_i].head.angle = Math.floor(angle);
                        nth.view.edges[edge_i].head.mag = Math.floor(mag);
                    }
                    nth.graphView.show(false);
                }
            },
            warn: function(message) {
                config.warn.call(null, message);
            },
            debug: function(message) {
                config.debug.call(null, message);
            },

            attachEnv: function(omee) {
                // save the outer most encapsulating element (omee)
                nth.graphView.omee = omee;
            },
            detachEnv: function() {
                // unbind here??
            },

            key_exists: function(key_str, obj) {
                if(typeof obj != "undefined" &&  typeof obj[key_str] != "undefined") {
                    return true;
                }
                return false;
            },
            get_property: function(obj, key_str_star) {
                if (arguments.length < 2) {return arguments[0];}
                var prop = arguments[0];
                var i = 1;

                while(i < arguments.length && typeof prop != "undefined") {
                    prop = prop[arguments[i]];
                    i += 1;
                }
                return prop;
            },
            // call nth.getConnectionPoint(edge_end=['head'|'tail'], node_view, edge_view) {
            getConnectionPoint: function(edge_end, node_view, edge_view) {
                var mag = config.gv.defaultEdge.magnitude;
                var angle_in_deg = (edge_end == 'head') ? 180 : 0;
                var angle = 0;
                var proportion = 1;
                var node_bb = nth.getBoundingBox(node_view);
                var x = node_bb.x;
                var y = node_bb.y;
                var width = node_bb.width;
                var height = node_bb.height;
                var cx = x + (width / 2);
                var cy = y + (height / 2);
                var controlPointXOffset;
                var controlPointYOffset;
                var pointX;
                var pointY;
                if (edge_view && edge_view[edge_end]) {
                    angle_in_deg = (nth.key_exists('angle', edge_view[edge_end])) ? edge_view[edge_end].angle : angle_in_deg;
                    mag = (nth.key_exists('mag', edge_view[edge_end])) ? edge_view[edge_end].mag : mag;
                }
                var shape = (nth.key_exists('shape', node_view)) ? node_view.shape : config.gv.defaultNode.shape;
                if (shape == 'circle') {
                    pointX = cx + Math.cos(angle_in_deg * 0.0174532925) * (width / 2);
                    pointY = cy + Math.sin(angle_in_deg * 0.0174532925) * (width / 2);
                }
                else
                    if (shape == 'rect' || shape == 'roundRect') {
                        // note: this is based on diagonal quadrants (not strict angle), -- better re-propotioning UI behavior (IMHO).
                        while (angle_in_deg < 0) {
                            angle_in_deg += 360;
                        }
                        angle_in_deg = angle_in_deg % 360; // nomalize
                        var lrx = x + width; // lower right x
                        var lry = y + height; // lower right y
                        var pad = (nth.key_exists('padding', node_view)) ? node_view.padding : config.gv.defaultNode.padding;
                        if (angle_in_deg >= 45 && angle_in_deg < 135) { //bottom
                            pointX = lrx - ((angle_in_deg - 45) / 90) * width;
                            pointY = lry;
                            if (shape == 'roundRect' && pad > 6) {
                                if (pointX < (x + pad)) { // left
                                    proportion = (pointX - x) / pad;
                                    angle = 135 - (proportion * 45);
                                    pointX = x + pad + Math.cos(angle * 0.0174532925) * pad;
                                    pointY = (lry - pad) + Math.sin(angle * 0.0174532925) * pad;
                                }
                                else
                                    if (pointX > (lrx - pad)) { // right
                                        proportion = (pointX - lrx) / pad;
                                        angle = 45 - (proportion * 45);
                                        pointX = (lrx - pad) + Math.cos(angle * 0.0174532925) * pad;
                                        pointY = (lry - pad) + Math.sin(angle * 0.0174532925) * pad;
                                    }
                            }
                        }
                        else
                            if (angle_in_deg >= 135 && angle_in_deg < 225) { //left
                                pointX = x;
                                pointY = (y + height) - ((angle_in_deg - 135) / 90) * height;
                                if (shape == 'roundRect' && pad > 6) {
                                    if (pointY < (y + pad)) { //  top
                                        proportion = (pointY - y) / pad;
                                        angle = 225 - (proportion * 45);
                                        pointX = x + pad + Math.cos(angle * 0.0174532925) * pad;
                                        pointY = y + pad + Math.sin(angle * 0.0174532925) * pad;
                                    }
                                    else
                                        if (pointY > (lry - pad)) { //  bottom
                                            proportion = (pointY - lry) / pad;
                                            angle = 135 - (proportion * 45);
                                            pointX = x + pad + Math.cos(angle * 0.0174532925) * pad;
                                            pointY = (lry - pad) + Math.sin(angle * 0.0174532925) * pad;
                                        }
                                }
                            }
                            else
                                if (angle_in_deg >= 225 && angle_in_deg < 315) { //top
                                    pointX = x + ((angle_in_deg - 225) / 90) * width;
                                    pointY = y;
                                    if (shape == 'roundRect' && pad > 6) {
                                        if (pointX < (x + pad)) { // left
                                            proportion = (pointX - x) / pad;
                                            angle = 225 + (proportion * 45);
                                            pointX = x + pad + Math.cos(angle * 0.0174532925) * pad;
                                            pointY = y + pad + Math.sin(angle * 0.0174532925) * pad;
                                        }
                                        else
                                            if (pointX > (lrx - pad)) { // right
                                                proportion = (pointX - lrx) / pad;
                                                angle = 315 + (proportion * 45);
                                                pointX = (lrx - pad) + Math.cos(angle * 0.0174532925) * pad;
                                                pointY = y + pad + Math.sin(angle * 0.0174532925) * pad;
                                            }
                                    }
                                }
                                else { // right
                                    pointX = lrx;
                                    pointY = y + ((angle_in_deg + 45) % 360 / 90) * height;
                                    if (shape == 'roundRect' && pad > 6) {
                                        if (pointY < (y + pad)) { //  top
                                            proportion = (pointY - y) / pad;
                                            angle = 315 + (proportion * 45);
                                            pointX = (lrx - pad) + Math.cos(angle * 0.0174532925) * pad;
                                            pointY = y + pad + Math.sin(angle * 0.0174532925) * pad;
                                        }
                                        else
                                            if (pointY > (lry - pad)) { //  bottom
                                                proportion = (pointY - lry) / pad;
                                                angle = 45 + (proportion * 45);
                                                pointX = (lrx - pad) + Math.cos(angle * 0.0174532925) * pad;
                                                pointY = (lry - pad) + Math.sin(angle * 0.0174532925) * pad;
                                            }
                                    }
                                }
                    }
                    else {
                        pointX = cx;
                        pointY = cy;
                    }

                controlPointXOffset = Math.cos(angle_in_deg * 0.0174532925) * mag;
                controlPointYOffset = Math.sin(angle_in_deg * 0.0174532925) * mag;
                return [{
                    x: pointX,
                    y: pointY
                }, {
                    x: pointX + controlPointXOffset,
                    y: pointY + controlPointYOffset
                }];
            },

            renderEdge: function (from, to, edge, edge_view, edge_index) {
                // TODO implement Knots, edge.knots ...
                var create = false;
                var stroke = edge.type=='get'? 'blue': 'green';
                stroke = edge.type=='set'? 'red': stroke;
                var stroke_width = edge_view['stroke-width']? edge_view['stroke-width']: 2;
                var arrow = (nth.key_exists('arrow', edge_view))? edge_view.arrow: config.gv.defaultEdge.arrow;
                if (arrow) {
                    if (to[1].x != to[0].x || to[1].y != to[0].y) {
                        var dx = to[0].x - to[1].x;
                        var dy = to[0].y - to[1].y;
                        // calculate angle and draw the arrow head
                        angle = nth.angleFromDelta(dx, dy);

                        var arrow_id = 'nth-'+config.gv.graph.name+'-arrow-edge-'+edge_index;
                        arrow = document.getElementById(arrow_id);
                        if (!arrow) {arrow = document.createElementNS(svgns, 'path'); create = true;}
                        d_str = "M-7 -3 L0 0 -7 3 Z";
                        arrow.setAttribute('class', 'nth-edge');
                        arrow.setAttribute('id', arrow_id);
                        arrow.setAttribute('d', d_str);
                        arrow.setAttribute('style', "fill:none;stroke:"+stroke+";stroke-width:"+stroke_width);
                        arrow.setAttribute("transform", "translate("+to[0].x+", "+to[0].y+"), rotate("+angle+")");
                        to[0].x = to[0].x - Math.cos(angle * 0.0174532925) * 7;
                        to[0].y = to[0].y - Math.sin(angle * 0.0174532925) * 7;
                        to[1].x = to[1].x - Math.cos(angle * 0.0174532925) * 7;
                        to[1].y = to[1].y - Math.sin(angle * 0.0174532925) * 7;
                    }
                }
                var el_id = 'nth-'+config.gv.graph.name+'-edge-'+edge_index;
                var el = document.getElementById(el_id);
                if (!el) {el = document.createElementNS(svgns, 'path'); create = true;}
                var d_str = "M"+from[0].x+" "+from[0].y+" C"+from[1].x+" "+from[1].y+" "+to[1].x+" "+to[1].y+" "+to[0].x+" "+to[0].y+" ";
                el.setAttribute('d', d_str);
                el.setAttribute('class', 'nth-edge');
                el.setAttribute('id', el_id);
                el.setAttribute('style', "fill:none;stroke:"+stroke+";stroke-width:"+stroke_width);

                var label_id = 'nth-'+config.gv.graph.name+'-label-edge-'+edge_index;
                var label = document.getElementById(label_id);
                if (!label) {label = document.createElementNS(svgns, 'text'); create = true;}

                label.setAttribute('class', label_id);
                label.setAttribute('id', label_id);
                label.setAttribute('x', (to[1].x + from[1].x)/2);
                label.setAttribute('y', (to[1].y + from[1].y)/2);
                label.setAttribute('fill', "#222");
                var label_text = nth.key_exists('ele', edge)? edge.ele: "";
                label_text = nth.key_exists('name', edge)? edge.name: label_text;
                //label_text = label_text==""? "edge: "+edge_index: label_text;
                $(label).text(label_text);
                if (create) {
                    return [el, arrow, label];
                }
                else {
                    return [false, false, false];
                }
            },

            angleFromDelta: function (x, y) {
                if (Math.abs(x) < 0.2) { // x is ~zero
                    if (Math.abs(y) < 0.2) {
                        return 0;
                    }
                    if (y > 0) {
                        return 90;
                    }
                    if (y < 0) {
                        return 270;
                    }
                }
                else if (x > 0) { // x is positive
                    if (y >= 0) {
                        return 180 * Math.atan(y / x) / Math.PI;
                    }
                    if (y < 0) {
                        return 180 * Math.atan(y / x) / Math.PI - (-1) * 360;
                    }
                }
                else { //(x < 0) x is negative
                    if (y >= 0) {
                        return 180 - 180 * Math.atan(-y / x) / Math.PI;
                    }
                    if (y < 0) {
                        return -180 - (-1) * 180 * Math.atan(y / x) / Math.PI - (-1) * 360;
                    }
                }
            },
            magnitudeFromDelta: function (x, y) {
                return Math.sqrt(x*x + y*y);
            },
            getBoundingBox: function (nv) {
                if(!nv.shape || nv.shape == 'circle') {
                    return {"x":nv.cx-nv.r, "y":nv.cy-nv.r, "width":nv.r*2, "height":nv.r*2};
                }
                else if(nv.shape == 'rect' || nv.shape == 'roundRect') {
                    return {"x":nv.x, "y":nv.y, "width":nv.w, "height":nv.h};
                }
                return {"x":10, "y":10, "width":30, "height":30};
            },
            removeNode: function (i) {
                // delete edges to and from this node.
                var edge_list = [];
                var backdrop_id = 'nth-'+config.gv.graph.name+'-backdrop';
                $.each(nth.graph.edges, function(edge_index, edge) {
                    if(edge.from == i || edge.to == i) {
                        edge_list.push(edge_index);
                    }
                });
                edge_list.reverse();
                $.each(edge_list, function(edge_list_i, e_i) {
                    nth.graph.edges.splice(e_i, 1);
                    nth.view.edges.splice(e_i, 1);
                });

                nth.graph.nodes.splice(i, 1);
                nth.view.nodes.splice(i, 1);
                // now deal with reassigning edge indexes > i
                $.each(nth.graph.edges, function(edge_index, edge) {
                    if(edge.from > i) {
                        nth.graph.edges[edge_index].from = edge.from-1}
                    if(edge.to > i) {
                        nth.graph.edges[edge_index].to = edge.to-1}
                });
                $(nth.graphView.omee).empty();
                nth.graphView.show();
            },
            removeEdge: function(i) {
                nth.graph.edges.splice(i, 1);
                nth.view.edges.splice(i, 1);
            }
        };

        // now start processing this nthGraph instance.
        if (settings) {$.extend(true, config, settings);}
        if (config.g && typeof config.g == 'object') {$.extend(true, nth.graph, config.g);}
        if (config.gv && typeof config.gv == 'object') {$.extend(true, nth.view, config.gv);}
        else if (config.cgref && typeof config.cgref == 'string') {
            nth.loadControlGraph(config.cgref);
        }
        else {nth.warn("No control graph (cg) indicated.");}

        // so far this is only tested on one (jQuery selected) element at a time,
        // but it could be used on an array of elements,
        // Input includes metadata (if the metadata.js lib is included),
        // or input is from form elements.
        // Output will be applied back to output decendant from the given element.
        this.each(function() {
            nth.graphView.show(this);
        });
        this.setGraph = function(gr, args) {
            nth.graph = gr;
            if (args && args.resetView === true) {nth.view = {};}
            nth.graphView.show(false);
        };
        this.getGraph = function() {
            return nth.graph;
        };
        this.setGraphView = function(gr_view) {
            nth.view = gr_view;
            $(nth.graphView.omee).empty();
            nth.graphView.show(false);
        };
        this.getGraphView = function() {
            return nth.view;
        };
        $("body").bind("write-graph-node-properties", function(event, i, node) {
            $.extend(nth.graph.nodes[i], node);
            $(nth.graphView.omee).empty();
            nth.graphView.show(false);
        });
        $("body").bind("write-graph-edge-properties", function(event, i, edge) {
            $.extend(nth.graph.edges[i], edge);
            $(nth.graphView.omee).empty();
            nth.graphView.show(false);
        });
        return this;
    };

})(jQuery);
